{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# Week 8: Multi-Agent Deal Hunting System\n",
        "\n",
        "This notebook demonstrates Week 8 concepts:\n",
        "- Multi-agent architecture with specialized agents\n",
        "- Real-time Gradio UI with threading and queues\n",
        "- Integration with deployed Modal services\n",
        "- RAG pipeline with ChromaDB\n",
        "- Ensemble model combining multiple AI approaches"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "!pip install -q gradio pydantic openai chromadb sentence-transformers scikit-learn feedparser beautifulsoup4 requests plotly"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "import os\n",
        "import logging\n",
        "import queue\n",
        "import threading\n",
        "import time\n",
        "import json\n",
        "from typing import List, Optional\n",
        "from datetime import datetime\n",
        "\n",
        "import gradio as gr\n",
        "import plotly.graph_objects as go\n",
        "from pydantic import BaseModel"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "class Deal(BaseModel):\n",
        "    product_description: str\n",
        "    price: float\n",
        "    url: str\n",
        "\n",
        "\n",
        "class DealSelection(BaseModel):\n",
        "    deals: List[Deal]\n",
        "\n",
        "\n",
        "class Opportunity(BaseModel):\n",
        "    deal: Deal\n",
        "    estimate: float\n",
        "    discount: float\n",
        "\n",
        "\n",
        "class MockAgent:\n",
        "    name = \"Mock Agent\"\n",
        "    color = '\\033[37m'\n",
        "    \n",
        "    def log(self, message):\n",
        "        logging.info(f\"[{self.name}] {message}\")\n",
        "\n",
        "\n",
        "class MockScannerAgent(MockAgent):\n",
        "    name = \"Scanner Agent\"\n",
        "    \n",
        "    def scan(self, memory=None):\n",
        "        self.log(\"Simulating RSS feed scan\")\n",
        "        time.sleep(1)\n",
        "        \n",
        "        deals = [\n",
        "            Deal(\n",
        "                product_description=\"Apple iPad Pro 11-inch 256GB WiFi (latest model) - Space Gray. Features M2 chip, Liquid Retina display, 12MP camera, Face ID, and all-day battery life.\",\n",
        "                price=749.99,\n",
        "                url=\"https://example.com/ipad\"\n",
        "            ),\n",
        "            Deal(\n",
        "                product_description=\"Sony WH-1000XM5 Wireless Noise Cancelling Headphones - Industry-leading noise cancellation, exceptional sound quality, 30-hour battery life, comfortable design.\",\n",
        "                price=329.99,\n",
        "                url=\"https://example.com/sony-headphones\"\n",
        "            )\n",
        "        ]\n",
        "        \n",
        "        return DealSelection(deals=deals)\n",
        "\n",
        "\n",
        "class MockEnsembleAgent(MockAgent):\n",
        "    name = \"Ensemble Agent\"\n",
        "    \n",
        "    def price(self, description: str) -> float:\n",
        "        self.log(f\"Estimating price for product\")\n",
        "        time.sleep(0.5)\n",
        "        \n",
        "        if \"iPad\" in description:\n",
        "            return 899.00\n",
        "        elif \"Sony\" in description:\n",
        "            return 398.00\n",
        "        else:\n",
        "            return 150.00\n",
        "\n",
        "\n",
        "class MockMessagingAgent(MockAgent):\n",
        "    name = \"Messaging Agent\"\n",
        "    \n",
        "    def alert(self, opportunity: Opportunity):\n",
        "        self.log(f\"Alert sent: ${opportunity.discount:.2f} discount on {opportunity.deal.product_description[:50]}...\")\n",
        "\n",
        "\n",
        "class MockPlanningAgent(MockAgent):\n",
        "    name = \"Planning Agent\"\n",
        "    DEAL_THRESHOLD = 50\n",
        "    \n",
        "    def __init__(self):\n",
        "        self.scanner = MockScannerAgent()\n",
        "        self.ensemble = MockEnsembleAgent()\n",
        "        self.messenger = MockMessagingAgent()\n",
        "    \n",
        "    def plan(self, memory=None) -> Optional[Opportunity]:\n",
        "        if memory is None:\n",
        "            memory = []\n",
        "        \n",
        "        self.log(\"Starting planning cycle\")\n",
        "        \n",
        "        selection = self.scanner.scan(memory)\n",
        "        \n",
        "        if selection and selection.deals:\n",
        "            opportunities = []\n",
        "            for deal in selection.deals:\n",
        "                estimate = self.ensemble.price(deal.product_description)\n",
        "                discount = estimate - deal.price\n",
        "                opportunities.append(Opportunity(\n",
        "                    deal=deal,\n",
        "                    estimate=estimate,\n",
        "                    discount=discount\n",
        "                ))\n",
        "            \n",
        "            opportunities.sort(key=lambda x: x.discount, reverse=True)\n",
        "            best = opportunities[0]\n",
        "            \n",
        "            self.log(f\"Best deal has discount: ${best.discount:.2f}\")\n",
        "            \n",
        "            if best.discount > self.DEAL_THRESHOLD:\n",
        "                self.messenger.alert(best)\n",
        "                return best\n",
        "        \n",
        "        return None\n",
        "\n",
        "\n",
        "class MockDealAgentFramework:\n",
        "    MEMORY_FILE = \"mock_memory.json\"\n",
        "    \n",
        "    def __init__(self):\n",
        "        self.memory = self.read_memory()\n",
        "        self.planner = None\n",
        "    \n",
        "    def init_agents_as_needed(self):\n",
        "        if not self.planner:\n",
        "            logging.info(\"Initializing Mock Agent Framework\")\n",
        "            self.planner = MockPlanningAgent()\n",
        "            logging.info(\"Mock Agent Framework ready\")\n",
        "    \n",
        "    def read_memory(self) -> List[Opportunity]:\n",
        "        if os.path.exists(self.MEMORY_FILE):\n",
        "            try:\n",
        "                with open(self.MEMORY_FILE, 'r') as f:\n",
        "                    data = json.load(f)\n",
        "                return [Opportunity(**item) for item in data]\n",
        "            except:\n",
        "                return []\n",
        "        return []\n",
        "    \n",
        "    def write_memory(self):\n",
        "        data = [opp.dict() for opp in self.memory]\n",
        "        with open(self.MEMORY_FILE, 'w') as f:\n",
        "            json.dump(data, f, indent=2)\n",
        "    \n",
        "    def run(self) -> List[Opportunity]:\n",
        "        self.init_agents_as_needed()\n",
        "        result = self.planner.plan(memory=self.memory)\n",
        "        \n",
        "        if result:\n",
        "            self.memory.append(result)\n",
        "            self.write_memory()\n",
        "        \n",
        "        return self.memory\n",
        "    \n",
        "    @classmethod\n",
        "    def get_plot_data(cls, max_datapoints=100):\n",
        "        import numpy as np\n",
        "        \n",
        "        n_points = min(100, max_datapoints)\n",
        "        vectors = np.random.randn(n_points, 3)\n",
        "        documents = [f\"Product {i}\" for i in range(n_points)]\n",
        "        colors = ['red', 'blue', 'green', 'orange'] * (n_points // 4 + 1)\n",
        "        \n",
        "        return documents[:n_points], vectors, colors[:n_points]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "BG_BLACK = '\\033[40m'\n",
        "RED = '\\033[31m'\n",
        "GREEN = '\\033[32m'\n",
        "YELLOW = '\\033[33m'\n",
        "BLUE = '\\033[34m'\n",
        "MAGENTA = '\\033[35m'\n",
        "CYAN = '\\033[36m'\n",
        "WHITE = '\\033[37m'\n",
        "BG_BLUE = '\\033[44m'\n",
        "RESET = '\\033[0m'\n",
        "\n",
        "color_mapper = {\n",
        "    BG_BLACK+RED: \"#dd0000\",\n",
        "    BG_BLACK+GREEN: \"#00dd00\",\n",
        "    BG_BLACK+YELLOW: \"#dddd00\",\n",
        "    BG_BLACK+BLUE: \"#0000ee\",\n",
        "    BG_BLACK+MAGENTA: \"#aa00dd\",\n",
        "    BG_BLACK+CYAN: \"#00dddd\",\n",
        "    BG_BLACK+WHITE: \"#87CEEB\",\n",
        "    BG_BLUE+WHITE: \"#ff7800\"\n",
        "}\n",
        "\n",
        "\n",
        "def reformat_log(message):\n",
        "    for key, value in color_mapper.items():\n",
        "        message = message.replace(key, f'<span style=\"color: {value}\">')\n",
        "    message = message.replace(RESET, '</span>')\n",
        "    return message\n",
        "\n",
        "\n",
        "class QueueHandler(logging.Handler):\n",
        "    def __init__(self, log_queue):\n",
        "        super().__init__()\n",
        "        self.log_queue = log_queue\n",
        "\n",
        "    def emit(self, record):\n",
        "        self.log_queue.put(self.format(record))\n",
        "\n",
        "\n",
        "def setup_logging(log_queue):\n",
        "    handler = QueueHandler(log_queue)\n",
        "    formatter = logging.Formatter(\n",
        "        \"[%(asctime)s] %(message)s\",\n",
        "        datefmt=\"%Y-%m-%d %H:%M:%S\",\n",
        "    )\n",
        "    handler.setFormatter(formatter)\n",
        "    logger = logging.getLogger()\n",
        "    logger.addHandler(handler)\n",
        "    logger.setLevel(logging.INFO)\n",
        "\n",
        "\n",
        "def html_for(log_data):\n",
        "    output = '<br>'.join(log_data[-20:])\n",
        "    return f\"\"\"\n",
        "    <div style=\"height: 420px; overflow-y: auto; border: 1px solid #444; background-color: #1a1a1a; padding: 12px; font-family: monospace; font-size: 13px; color: #fff;\">\n",
        "        {output}\n",
        "    </div>\n",
        "    \"\"\"\n",
        "\n",
        "\n",
        "def get_plot():\n",
        "    try:\n",
        "        documents, vectors, colors = MockDealAgentFramework.get_plot_data(max_datapoints=100)\n",
        "        \n",
        "        fig = go.Figure(data=[go.Scatter3d(\n",
        "            x=vectors[:, 0],\n",
        "            y=vectors[:, 1],\n",
        "            z=vectors[:, 2],\n",
        "            mode='markers',\n",
        "            marker=dict(size=3, color=colors, opacity=0.7),\n",
        "        )])\n",
        "        \n",
        "        fig.update_layout(\n",
        "            scene=dict(\n",
        "                xaxis_title='X', \n",
        "                yaxis_title='Y', \n",
        "                zaxis_title='Z',\n",
        "                aspectmode='manual',\n",
        "                aspectratio=dict(x=2.2, y=2.2, z=1),\n",
        "                camera=dict(eye=dict(x=1.6, y=1.6, z=0.8)),\n",
        "                bgcolor='#1a1a1a'\n",
        "            ),\n",
        "            height=420,\n",
        "            margin=dict(r=5, b=5, l=5, t=5),\n",
        "            paper_bgcolor='#1a1a1a',\n",
        "            font=dict(color='#ffffff'),\n",
        "            title=\"Mock Vector Space (Random Data for Demo)\"\n",
        "        )\n",
        "        return fig\n",
        "        \n",
        "    except Exception as e:\n",
        "        fig = go.Figure()\n",
        "        fig.update_layout(\n",
        "            title=f'Error: {str(e)}',\n",
        "            height=420,\n",
        "            paper_bgcolor='#1a1a1a',\n",
        "            font=dict(color='#ffffff')\n",
        "        )\n",
        "        return fig"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "class DealHunterApp:\n",
        "    \n",
        "    def __init__(self):\n",
        "        self.framework = None\n",
        "    \n",
        "    def get_framework(self):\n",
        "        if not self.framework:\n",
        "            self.framework = MockDealAgentFramework()\n",
        "            self.framework.init_agents_as_needed()\n",
        "        return self.framework\n",
        "    \n",
        "    def opportunities_to_table(self, opportunities):\n",
        "        if not opportunities:\n",
        "            return []\n",
        "        \n",
        "        return [\n",
        "            [\n",
        "                opp.deal.product_description,\n",
        "                f\"${opp.deal.price:.2f}\",\n",
        "                f\"${opp.estimate:.2f}\",\n",
        "                f\"${opp.discount:.2f}\",\n",
        "                opp.deal.url\n",
        "            ]\n",
        "            for opp in opportunities\n",
        "            if isinstance(opp, Opportunity)\n",
        "        ]\n",
        "    \n",
        "    def scan_for_deals(self):\n",
        "        framework = self.get_framework()\n",
        "        logging.info(f\"Scan triggered at {datetime.now().strftime('%H:%M:%S')} - Current memory: {len(framework.memory)} deals\")\n",
        "        new_opportunities = framework.run()\n",
        "        logging.info(f\"Scan complete - Total deals: {len(framework.memory)}\")\n",
        "        return self.opportunities_to_table(new_opportunities)\n",
        "    \n",
        "    def scan_with_logging(self, log_data):\n",
        "        log_queue = queue.Queue()\n",
        "        result_queue = queue.Queue()\n",
        "        setup_logging(log_queue)\n",
        "        \n",
        "        def worker():\n",
        "            result = self.scan_for_deals()\n",
        "            result_queue.put(result)\n",
        "        \n",
        "        thread = threading.Thread(target=worker)\n",
        "        thread.start()\n",
        "        \n",
        "        framework = self.get_framework()\n",
        "        current_table = self.opportunities_to_table(framework.memory)\n",
        "        \n",
        "        while True:\n",
        "            try:\n",
        "                message = log_queue.get_nowait()\n",
        "                log_data.append(reformat_log(message))\n",
        "                current_table = self.opportunities_to_table(framework.memory)\n",
        "                yield log_data, html_for(log_data), current_table\n",
        "            except queue.Empty:\n",
        "                try:\n",
        "                    final_table = result_queue.get_nowait()\n",
        "                    yield log_data, html_for(log_data), final_table\n",
        "                    return\n",
        "                except queue.Empty:\n",
        "                    current_table = self.opportunities_to_table(framework.memory)\n",
        "                    yield log_data, html_for(log_data), current_table\n",
        "                    time.sleep(0.1)\n",
        "    \n",
        "    def handle_selection(self, selected_index: gr.SelectData):\n",
        "        framework = self.get_framework()\n",
        "        row = selected_index.index[0]\n",
        "        \n",
        "        if row < len(framework.memory):\n",
        "            opportunity = framework.memory[row]\n",
        "            framework.planner.messenger.alert(opportunity)\n",
        "            return f\"Alert sent for: {opportunity.deal.product_description[:60]}...\"\n",
        "        \n",
        "        return \"Invalid selection\"\n",
        "    \n",
        "    def load_initial_state(self):\n",
        "        framework = self.get_framework()\n",
        "        initial_table = self.opportunities_to_table(framework.memory)\n",
        "        return [], \"\", initial_table\n",
        "    \n",
        "    def launch(self):\n",
        "        with gr.Blocks(title=\"The Price is Right\", fill_width=True) as ui:\n",
        "            \n",
        "            log_data = gr.State([])\n",
        "            \n",
        "            gr.Markdown(\n",
        "                '<div style=\"text-align: center; font-size: 28px; font-weight: bold; margin: 20px 0;\">The Price is Right - Demo</div>'\n",
        "                '<div style=\"text-align: center; font-size: 16px; color: #666; margin-bottom: 20px;\">Multi-Agent Deal Hunting System (Mock Version)</div>'\n",
        "            )\n",
        "            \n",
        "            with gr.Row():\n",
        "                opportunities_table = gr.Dataframe(\n",
        "                    headers=[\"Product Description\", \"Price\", \"Estimate\", \"Discount\", \"URL\"],\n",
        "                    wrap=True,\n",
        "                    column_widths=[6, 1, 1, 1, 3],\n",
        "                    row_count=10,\n",
        "                    col_count=5,\n",
        "                    max_height=420,\n",
        "                    interactive=False\n",
        "                )\n",
        "            \n",
        "            with gr.Row():\n",
        "                with gr.Column(scale=1):\n",
        "                    logs_display = gr.HTML(label=\"Agent Logs\")\n",
        "                with gr.Column(scale=1):\n",
        "                    vector_plot = gr.Plot(value=get_plot(), show_label=False)\n",
        "            \n",
        "            ui.load(\n",
        "                self.load_initial_state,\n",
        "                inputs=[],\n",
        "                outputs=[log_data, logs_display, opportunities_table]\n",
        "            )\n",
        "            \n",
        "            timer = gr.Timer(value=10, active=True)\n",
        "            timer.tick(\n",
        "                self.scan_with_logging,\n",
        "                inputs=[log_data],\n",
        "                outputs=[log_data, logs_display, opportunities_table]\n",
        "            )\n",
        "            \n",
        "            selection_feedback = gr.Textbox(visible=False)\n",
        "            opportunities_table.select(\n",
        "                self.handle_selection,\n",
        "                inputs=[],\n",
        "                outputs=[selection_feedback]\n",
        "            )\n",
        "        \n",
        "        ui.launch(share=True, inbrowser=True)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "app = DealHunterApp()\n",
        "app.launch()"
      ]
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "env",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.13.0"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 2
}
